#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 ZHENG Zhong <heavyzheng nospam-at gmail D0T com>
# http://www.zhengzhong.net/
#
# This script is used to change project conventions.
# 
# $Id$
#

from __future__ import with_statement

import logging
import os
import re
import sys
import traceback
import yaml

from optparse import OptionParser

#---------------------------------------------------------------------------------------------------
# NameProcessor and sub-classes
#---------------------------------------------------------------------------------------------------

class NameProcessor(object):
    """Pure abstract base class for processing a name of a file or a directory."""
    
    def process(self, file):
        """Processes the name of the specified file.
        Arguments:
            file : the file whose name is to be processed.
        Returns:
            The processing result as a 2-valued tuple: item 0 is a boolean indicating if the file
            should be ignored; item 1 is the new file name to use in place of the original one.
        """
        raise NotImplementedError("Method process from NameProcessor needs to be implemented.")


class Ignore(NameProcessor):

    def process(self, file):
        return (False, None)


class Rename(NameProcessor):

    def __init__(self, rename_to):
        self.__rename_to = rename_to

    def process(self, file):
        return (True, self.__rename_to)


class ReplaceNamePrefix(NameProcessor):

    def __init__(self, prefix, replace_by):
        self.__prefix = prefix
        self.__replace_by = replace_by
    
    def process(self, file):
        basename = os.path.basename(file)
        if basename.startswith(self.__prefix):
            return (True, basename.replace(self.__prefix, self.__replace_by, 1))
        else:
            return (True, basename)


#---------------------------------------------------------------------------------------------------
# ContentProcessor and sub-classes
#---------------------------------------------------------------------------------------------------

class ContentProcessor(object):
    """Pure abstract base class for processinging the content of a file."""
    
    def process(self, content):
        """Processes the specified content.
        Arguments:
            content : the content to process.
        Returns:
            The processing result as a 2-valued tuple: item 0 is a boolean indicating if the
            content should be ignored; item 1 is the new content to replace the original one.
        """
        raise NotImplementedError("Method process from ContentProcessor needs to be implemented.")


class InsertHeader(ContentProcessor):

    def __init__(self, header):
        self.__header = header
    
    def process(self, content):
        return (True, self.__header + content)


class ReplaceKeyword(ContentProcessor):

    def __init__(self, keyword, replace_by):
        self.__keyword = keyword
        self.__replace_by = replace_by
    
    def process(self, content):
        return (True, content.replace(self.__keyword, self.__replace_by))


class ReplaceLineByPrefix(ContentProcessor):

    def __init__(self, prefix, replace_by):
        self.__prefix = prefix
        self.__replace_by = replace_by
    
    def process(self, content):
        lines = content.split("\n")
        new_lines = []
        for line in lines:
            if line.startswith(self.__prefix):
                new_lines.append(self.__replace_by)
            else:
                new_lines.append(line)
        return (True, "\n".join(new_lines))


class IgnoreLineByPrefix(ContentProcessor):

    def __init__(self, prefix):
        self.__prefix = prefix
    
    def process(self, content):
        lines = content.split("\n")
        new_lines = []
        for line in lines:
            if not line.startswith(self.__prefix):
                new_lines.append(line)
        return (True, "\n".join(new_lines))


#---------------------------------------------------------------------------------------------------
# BoostifyError
#---------------------------------------------------------------------------------------------------

class BoostifyError(Exception):
    pass

#---------------------------------------------------------------------------------------------------
# Boostifier
#---------------------------------------------------------------------------------------------------

class Boostifier(object):

    def __init__(self):
        self.__name_processors = []
        self.__content_processors = []
    
    
    def load_config(self, config_file):
        """Loads configurations from a YAML file."""
        
        logging.info("Loading configurations from '%s'..." % config_file)
        
        # Clear name processors and content processors.
        del self.__name_processors[:]
        del self.__content_processors[:]
        
        try:
            # Load the YAML configuration file.
            with open(config_file, "r") as f:
                config = yaml.load(f)
            
            for config_entry in config:
            
                # Get the processor name, acceptable pattern, and kargs.
                name = config_entry.get("processor")
                pattern = config_entry.get("pattern", ".*")
                kargs = config_entry.get("properties", {})
                
                # Construct processor object by name.
                if name == "Ignore":
                    processor = Ignore(**kargs)
                elif name == "Rename":
                    processor = Rename(**kargs)
                elif name == "ReplaceNamePrefix":
                    processor = ReplaceNamePrefix(**kargs)
                elif name == "InsertHeader":
                    processor = InsertHeader(**kargs)
                elif name == "ReplaceKeyword":
                    processor = ReplaceKeyword(**kargs)
                elif name == "ReplaceLineByPrefix":
                    processor = ReplaceLineByPrefix(**kargs)
                elif name == "IgnoreLineByPrefix":
                    processor = IgnoreLineByPrefix(**kargs)
                else:
                    processor = None
                
                # Append the pattern and associated processor to processor lists.
                if processor:
                    if isinstance(processor, NameProcessor):
                        self.__name_processors.append( (re.compile(pattern), processor) )
                    elif isinstance(processor, ContentProcessor):
                        self.__content_processors.append( (re.compile(pattern), processor) )
                    else:
                        message = "Failed to determine processor type: '%s'." % type(processor)
                        logging.warning(message)
                else:
                    message = "Failed to determine processor type by name: '%s'." % name
                    logging.warning(message)
            
            # Print information.
            logging.info( "Configured %d name processors and %d content processors."
                        % (len(self.__name_processors), len(self.__content_processors)) )
        
        except Exception, ex:
            message = "Failed to load config file '%s': %s" % (config_file, str(ex))
            logging.error(message)
            raise
    
    
    def process(self, src_dir, dst_dir):
        """Processes the source directory and generates result to the destination directory.
        Arguments:
            src_dir : the source directory as a string.
            dst_dir : the destination directory as a string.
        """
        
        logging.info("Starting to boosify '%s' to '%s'..." % (src_dir, dst_dir))
        
        # Check if src_dir is a directory.
        if not os.path.isdir(src_dir):
            error = "Source '%s' is not a directory." % src_dir
            logging.error(error)
            raise BoostifyError, error
        
        # Make sure dst_dir is a directory and exists.
        if not os.path.exists(dst_dir):
            logging.debug("Making destination directories: '%s'" % dst_dir)
            os.makedirs(dst_dir)
        if not os.path.isdir(dst_dir):
            error = "Destination '%s' is not a directory." % dst_dir
            logging.error(error)
            raise BoostifyError, error
        
        # Process the source directory to destination directory, recursively.
        self.__process_recursive(src_dir, dst_dir)
    
    
    def __process_recursive(self, src, dst_dir):
        """Processes the source file or directory and generates result to the base destination
        directory. This method processes the source name to determinate the destination name, then
        delegates to __process_dir or __process_file, depending on the source type.
        Arguments:
            src     : the source file or directory to process.
            dst_dir : the base destination directory.
        """
        
        logging.info("Processing: %s" % src)
        
        # Pass the source file or directory through the NameProcessor objects.
        processor = self.__find_name_processor(src)
        if processor:
            (ok, basename) = processor.process(src)
        else:
            ok = True
            basename = os.path.basename(src)
        
        if ok:
            dst = os.path.join(dst_dir, basename)
            if os.path.isdir(src):
                self.__process_dir(src, dst)
            elif os.path.isfile(src):
                self.__process_file(src, dst)
            else:
                logging.warning("'%s' is neither directory nor file: ignored." % src)
        else:
            logging.debug("'%s' ignored by NameProcessor." % src)
    
    
    def __process_dir(self, src_dir, dst_dir):
        """Processes a directory. This method firstly makes sure the destination directory exists
        (otherwise, it will be created), and then calls __process_recursive on each file and
        sub-directory under the source directory.
        Arguments:
            src_dir : the source directory to process.
            dst_dir : the destination directory.
        """
        
        # If the destination directory does not exist, create it.
        if not os.path.exists(dst_dir):
            logging.debug("Making directories: '%s'..." % dst_dir)
            os.makedirs(dst_dir)
        
        # Process files and sub-directories under the source directory, recursively.
        if os.path.isdir(dst_dir):
            for file in os.listdir(src_dir):
                src_file = os.path.join(src_dir, file)
                self.__process_recursive(src_file, dst_dir)
        else:
            error = "Failed to processing '%s': destination '%s' is not a directory." \
                  % (src_dir, dst_dir)
            logging.error(error)
    
    
    def __process_file(self, src_file, dst_file):
        """Processes the specified source file to the destination file. This method reads the
        content of the source file, passes the content through a list of ContentProcessor objects,
        and writes the processed content to the destination file.
        Arguments:
            src_file : the source file to process.
            dst_file : the destination file.
        """
        
        try:
            # Read the content of the source file.
            with open(src_file, "r") as f:
                content = f.read()
            # Pass the content through the ContentProcessor objects.
            processors = self.__find_content_processors(src_file)
            ok = True
            for processor in processors:
                if ok:
                    (ok, content) = processor.process(content)
            # Write the processed content to the destination file.
            if ok:
                with open(dst_file, "w") as f:
                    f.write(content)
            else:
                logging.info("Source file '%s' ignored by ContentProcessor." % src_file)
        except Exception, ex:
            logging.error("Failed to process '%s' to '%s': %s" % (src_file, dst_file, str(ex)))
    
    
    def __find_name_processor(self, src_file):
        basename = os.path.basename(src_file)
        for item in self.__name_processors:
            if item[0].search(basename):
                return item[1]
        return None
    
    def __find_content_processors(self, src_file):
        processors = []
        basename = os.path.basename(src_file)
        for item in self.__content_processors:
            if item[0].search(basename):
                processors.append(item[1])
        return processors


#---------------------------------------------------------------------------------------------------
# Main functions
#---------------------------------------------------------------------------------------------------

def _create_option_parser():
    """Creates an option parser to parse command-line arguments."""

    parser = OptionParser(usage="""%prog [OPTIONS] SOURCE_DIRECTORY
    Change project conventions for the specified SOURCE_DIRECTORY.""")
    parser.add_option( "-c", "--config",
                       dest="config",
                       metavar="CONFIG",
                       help="config file containing convention definitions" )
    parser.add_option( "-o", "--output",
                       dest="output",
                       metavar="OUTPUT",
                       default="./~boostified",
                       help="output directory, default to 'boostified'" )
    parser.add_option( "-q", "--quiet",
                       dest="quiet",
                       default=False,
                       action="store_true",
                       help="set to quiet mode, print only warnings and errors" )
    parser.add_option( "-v", "--verbose",
                       dest="verbose",
                       default=False,
                       action="store_true",
                       help="set to verbose mode, print detailed messages" )
    return parser


def main(argv):

    # Parse command-line arguments to opts and args:
    # - opts contains 4 members: config (required), output (required), quiet, verbose;
    # - args contains 1 item which is the source directory to process.
    parser = _create_option_parser()
    (opts, args) = parser.parse_args(argv)

    if opts.config and opts.output and len(args) == 1:
        # Configure logging, set logging level.
        logging.basicConfig(format="%(levelname)-7s: %(message)s")
        if opts.quiet:
            logging.getLogger().setLevel(logging.WARNING)
        elif opts.verbose:
            logging.getLogger().setLevel(logging.DEBUG)
        else:
            logging.getLogger().setLevel(logging.INFO)
        # Boostify.
        try:
            boostifier = Boostifier()
            boostifier.load_config(opts.config)
            boostifier.process(args[0], opts.output)
            logging.info("Done.")
        except Exception, ex:
            logging.error("Abort: an error has occurred during processing.")
            # Print exception traceback.
            print >> sys.stderr, "-" * 60
            traceback.print_exc()
            print >> sys.stderr, "-" * 60
    
    else:
        # Invalid command-line arguments: print error message(s) and help.
        print >> sys.stderr, "\n"
        if not opts.config:
            print >> sys.stderr, "Error: config file not specified."
        if not opts.output:
            print >> sys.stderr, "Error: output directory not specified."
        if len(args) < 1:
            print >> sys.stderr, "Error: source directory not specified."
        elif len(args) > 1:
            print >> sys.stderr, "Error: too many source directories."
        print >> sys.stderr, "\n"
        parser.print_help()


#---------------------------------------------------------------------------------------------------

if __name__ == "__main__":
    main(sys.argv[1:])


